The programming model of queued calls described so far was
one-sided: the client posted a one-way message to a queue, and the
service processed that message. This model is sufficient when the queued
operations are one-way calls by nature. However, the queued service may
need to report back to its client on the result of the invocation, or
return results or even errors. By default, this is not possible: WCF
equates queued calls with one-way calls, which inherently forbids any
such response. In addition, queued services (and their clients) are
potentially disconnected. If a client posts a queued call to a
disconnected service, by the time the service finally gets the message
and processes it, there may no longer be a client to return the values
to. The solution is to have the service report back to a client-provided
queued service.Figure 1shows the
architecture of such a solution.
The response service is just another queued service in the system.
The response service may be disconnected toward the client as well, or
it may share the client’s process, or it may be hosted in a separate
process or even on a separate machine. If the response service shares
the client’s process, when the client is launched the response service
will start processing the queued responses. Having the response service
in a separate process (or even on a separate machine) from the client’s
helps to further decouple lifeline-wise the response service from the
client or clients that use it.
Note:
Not all queued services require a response service. Be
pragmatic, and use a response service only where appropriate; that is,
where it adds the most value.
1. Designing a Response Service Contract
As with any WCF service, the client and the service need to
agree beforehand on the response contract and what it will be used
for; that is, whether it will be used for returned values and error
information, or just returned values. Note that you can also split the
response service into two services, and have one response service for
results and another for faults and errors. As an example, consider the
ICalculator contract implemented by
the queued MyCalculator
service:
[ServiceContract]
interface ICalculator
{
[OperationContract(IsOneWay = true)]
void Add(int number1,int number2);
//More operations
}
class MyCalculator : ICalculator
{...}
The MyCalculator service is
required to respond to its client with the result of the calculation
and report on any errors. The result of the calculation is an integer,
and the error is in the form of the ExceptionDetail data contract . The ICalculatorResponse contract could be
defined as:
[ServiceContract]
interface ICalculatorResponse
{
[OperationContract(IsOneWay = true)]
void OnAddCompleted(int result,ExceptionDetail error);
//More operations
}
The response service supporting
ICalculatorResponse needs to
examine the returned error information; notify the client application,
the user, or the application administrator on the method completion;
and make the results available to the interested parties. Example 1 shows a simple response service
that supports ICalculatorResponse.
Example 1. A simple response service
class MyCalculatorResponse : ICalculatorResponse
{
public void OnAddCompleted(int result,ExceptionDetail error)
{
if(error != null)
{
//Handle error
}
else
{
MessageBox.Show("Result = " + result,"MyCalculatorResponse");
}
}
//More operations
}
|
As demonstrated by Example 1,
the response service is just that—a simple service. There is nothing
special about it other than its designation as a response
service.
1.1. Response address and method ID
There are two immediate problems with the implementation of
both MyCalculator and MyCalculatorResponse. The first is that
the same response service could be used to handle the response (or
completion) of multiple calls on multiple queued services, and yet,
as listed in Example 9-21, MyCalculatorResponse (and more
importantly, the clients it serves) has no way of distinguishing
between responses. The solution for that is to have the client that
issued the original queued call tag the call by associating it with
some unique ID, or at least an ID that is unique enough across that
client’s application. The queued service MyCalculator needs to pass that ID to the
response service MyCalculatorResponse, so that it can apply its
custom logic regarding that ID. Note that the service typically has
no direct use for the ID; all it needs to do is pass it
along.
The second problem is how to enable the queued service to
discover the address of the response service. Unlike with duplex
callbacks, there is no built-in support in WCF for passing the
response service’s reference to the queued service, so the queued
service needs to manually construct a proxy to the response service
and invoke the operations of the response contract. While the
response contract is decided upon at design time, and the binding is
always NetMsmqBinding, the
queued service lacks the address of the response service to be able
to respond. You could place that address in the service host config
file (in a client section) but
such a course of action is to be avoided. The main reason is that
the same queued service could be called by multiple clients, each
with its own dedicated response service and address.
One possible solution is to explicitly pass both the
client-managed ID and the desired response service address as
parameters to every operation on the queued service contract:
[ServiceContract]
interface ICalculator
{
[OperationContract(IsOneWay = true)]
void Add(int number1,int number2,string responseAddress,string methodId);
}
Much the same way, the queued service could explicitly pass
the method ID to the response service as a parameter to every
operation on the queued response contract:
[ServiceContract]
interface ICalculatorResponse
{
[OperationContract(IsOneWay = true)]
void OnAddCompleted(int result,ExceptionDetail error,string methodId);
}
1.2. The ResponseContext class
While passing the address and the ID as explicit
parameters would work, it does distort the original contract, and it
introduces plumbing-level parameters alongside business-level
parameters in the same operation. A better solution is to have the
client store the response address and operation ID in the outgoing
message headers of the call. Using the message headers this way is a
general-purpose technique for passing out-of-band information to the
service (information that is otherwise not present in the service
contract).
Since the client needs to pass both the address and the method
ID in the message headers, a single primitive type parameter will
not do. Instead, use my ResponseContext class, defined in Example 2.
Example 2. The ResponseContext class
[DataContract]
public class ResponseContext
{
[DataMember]
public readonly string ResponseAddress;
[DataMember]
public readonly string FaultAddress;
[DataMember]
public readonly string MethodId;
public ResponseContext(string responseAddress,string methodId) :
this(responseAddress,methodId,null)
{}
public ResponseContext(string responseAddress) : this(responseAddress,
Guid.NewGuid().ToString())
{}
public ResponseContext(string responseAddress,string methodId,
string faultAddress)
{
ResponseAddress = responseAddress;
MethodId = methodId;
FaultAddress = faultAddress;
}
public static ResponseContext Current
{
get
{
return GenericContext<ResponseContext>.Current.Value;
}
set
{
GenericContext<ResponseContext>.Current =
new GenericContext<ResponseContext>(value);
}
}
//More members
}
|
ResponseContext provides a
place to store both the response address and the ID. In addition, if
the client wants to use a separate response service for faults,
ResponseContext provides a field for the
fault response service address. The client is responsible for constructing an instance of
ResponseContext with a unique ID. While the
client can supply that ID as a construction parameter, the client
can also use the constructor of ResponseContext, which takes just the
response address, and have that constructor generate a GUID for the
ID. To streamline the act of storing a ResponseContext instance in and retrieving
it from the headers, ResponseContext provides the Current property, which merely
encapsulates my GenericContext<T>. The client can
provide an ID for each method call (even when dealing with a
sessionful queued service) by using a different instance of ResponseContext for
each call.